/**
 * Test the write conflict behavior between transactional and non-transactional (single document)
 * writes.
 *
 * All writes in MongoDB execute inside transactions. Single document writes (which, until 4.0,
 * categorized all writes), will indefinitely retry, if their associated transaction encounters a
 * WriteConflict error. This differs from the behavior of multi-document transactions, where
 * WriteConflict exceptions that occur inside a transaction are not automatically retried, and are
 * returned to the client. This means that writes to a document D inside a multi-document
 * transaction will effectively "block" any subsequent single document writes to D, until the
 * multi-document transaction commits.
 *
 * Note that in this test we sometimes refer to a single document write as "non-transactional".
 * Internally, single document writes still execute inside a transaction, but we use this
 * terminology to distinguish them from multi-document transactions.
 *
 * @tags: [uses_transactions]
 */

import {withRetryOnTransientTxnError} from "jstests/libs/auto_retry_transaction_in_sharding.js";
import {Thread} from "jstests/libs/parallelTester.js";

const dbName = "test";
const collName = "write_conflicts_with_non_txns";

const testDB = db.getSiblingDB(dbName);
const testColl = testDB[collName];

// Clean up and create test collection.
testDB.runCommand({drop: collName, writeConcern: {w: "majority"}});
assert.commandWorked(testDB.runCommand({create: collName, writeConcern: {w: "majority"}}));

const sessionOptions = {
    causalConsistency: false
};
const session = db.getMongo().startSession(sessionOptions);
const sessionDb = session.getDatabase(dbName);
const sessionColl = sessionDb[collName];

// Two conflicting documents to be inserted by a multi-document transaction and a
// non-transactional write, respectively.
const txnDoc = {
    _id: 1
};
const nonTxnDoc = {
    _id: 1,
    nonTxn: true
};

// Performs a single document insert on the test collection. Returns the command result object.
function singleDocWrite(dbName, collName, doc) {
    const testColl = db.getSiblingDB(dbName)[collName];
    return testColl.runCommand({insert: collName, documents: [doc]});
}

// Returns true if a single document insert has started running on the server.
function writeStarted() {
    return testDB.currentOp().inprog.some(op => {
        return op.active && (op.ns === testColl.getFullName()) && (op.op === "insert") &&
            (op.writeConflicts > 0);
    });
}

/**
 * A non-transactional (single document) write should keep retrying when attempting to insert a
 * document that conflicts with a previous write done by a running transaction, and should be
 * allowed to continue after the transaction commits. If 'maxTimeMS' is specified, a single
 * document write should timeout after the given time limit if there is a write conflict.
 */

jsTestLog("Start a multi-document transaction with a document insert.");
session.startTransaction();
assert.commandWorked(sessionColl.insert(txnDoc));

jsTestLog("Do a conflicting single document insert outside of transaction with maxTimeMS.");
assert.commandFailedWithCode(
    testColl.runCommand({insert: collName, documents: [nonTxnDoc], maxTimeMS: 100}),
    ErrorCodes.MaxTimeMSExpired);

jsTestLog("Doing conflicting single document write in separate thread.");
let thread = new Thread(singleDocWrite, dbName, collName, nonTxnDoc);
thread.start();

// Wait for the single doc write to start.
assert.soon(writeStarted);

// Commit the transaction, which should allow the single document write to finish. Since the
// single doc write should get serialized after the transaction, we expect it to fail with a
// duplicate key error.
jsTestLog("Commit the multi-document transaction.");
assert.commandWorked(session.commitTransaction_forTesting());
thread.join();
assert.commandFailedWithCode(thread.returnData(), ErrorCodes.DuplicateKey);

// Check the final documents.
assert.sameMembers([txnDoc], testColl.find().toArray());

// Clean up the test collection.
assert.commandWorked(testColl.remove({}));

/**
 * A non-transactional (single document) write should keep retrying when attempting to insert a
 * document that conflicts with a previous write done by a running transaction, and should be
 * allowed to continue and complete successfully after the transaction aborts.
 */

jsTestLog("Start a multi-document transaction with a document insert.");
session.startTransaction();
assert.commandWorked(sessionColl.insert(txnDoc));

jsTestLog("Doing conflicting single document write in separate thread.");
thread = new Thread(singleDocWrite, dbName, collName, nonTxnDoc);
thread.start();

// Wait for the single doc write to start.
assert.soon(writeStarted);

// Abort the transaction, which should allow the single document write to finish and insert its
// document successfully.
jsTestLog("Abort the multi-document transaction.");
assert.commandWorked(session.abortTransaction_forTesting());
thread.join();
assert.commandWorked(thread.returnData());

// Check the final documents.
assert.sameMembers([nonTxnDoc], testColl.find().toArray());

// Clean up the test collection.
assert.commandWorked(testColl.remove({}));

/**
 * A transaction that tries to write to a document that was updated by a non-transaction after
 * it started should fail with a WriteConflict.
 */

jsTestLog("Start a multi-document transaction.");
withRetryOnTransientTxnError(
    () => {
        session.startTransaction();
        assert.commandWorked(sessionColl.runCommand({find: collName}));

        jsTestLog("Do a single document insert outside of the transaction.");
        assert.commandWorked(testColl.insert(nonTxnDoc));

        jsTestLog("Insert a conflicting document inside the multi-document transaction.");
        assert.commandFailedWithCode(sessionColl.insert(txnDoc), ErrorCodes.WriteConflict);
        assert.commandFailedWithCode(session.commitTransaction_forTesting(),
                                     ErrorCodes.NoSuchTransaction);
    },
    () => {
        session.abortTransaction_forTesting();
    });

// Check the final documents.
assert.sameMembers([nonTxnDoc], testColl.find().toArray());
